/*
Copyright 2024 New Vector Ltd.
Copyright 2022, 2023 The Matrix.org Foundation C.I.C.

SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/

import React from "react";
import {
    type IPushRule,
    type IPushRules,
    RuleId,
    type IPusher,
    LOCAL_NOTIFICATION_SETTINGS_PREFIX,
    MatrixEvent,
    Room,
    PushRuleActionName,
    TweakName,
    ConditionKind,
    type IPushRuleCondition,
    PushRuleKind,
    type IThreepid,
    ThreepidMedium,
} from "matrix-js-sdk/src/matrix";
import { secureRandomString } from "matrix-js-sdk/src/randomstring";
import {
    act,
    fireEvent,
    getByTestId,
    render,
    screen,
    waitFor,
    waitForElementToBeRemoved,
    within,
} from "jest-matrix-react";
import { mocked } from "jest-mock";
import userEvent from "@testing-library/user-event";
import { PushProcessor } from "matrix-js-sdk/src/pushprocessor";
import { Form } from "@vector-im/compound-web";

import Notifications from "../../../../../src/components/views/settings/Notifications";
import SettingsStore from "../../../../../src/settings/SettingsStore";
import { StandardActions } from "../../../../../src/notifications/StandardActions";
import {
    clearAllModals,
    getMockClientWithEventEmitter,
    mkMessage,
    mockClientMethodsUser,
} from "../../../../test-utils";

// don't pollute test output with error logs from mock rejections
jest.mock("matrix-js-sdk/src/logger");

// Avoid indirectly importing any eagerly created stores that would require extra setup
jest.mock("../../../../../src/Notifier");

const masterRule: IPushRule = {
    actions: [PushRuleActionName.DontNotify],
    conditions: [],
    default: true,
    enabled: false,
    rule_id: RuleId.Master,
};
const oneToOneRule: IPushRule = {
    conditions: [
        { kind: ConditionKind.RoomMemberCount, is: "2" },
        { kind: ConditionKind.EventMatch, key: "type", pattern: "m.room.message" },
    ],
    actions: [PushRuleActionName.Notify, { set_tweak: TweakName.Highlight, value: false }],
    rule_id: ".m.rule.room_one_to_one",
    default: true,
    enabled: true,
} as IPushRule;
const encryptedOneToOneRule: IPushRule = {
    conditions: [
        { kind: ConditionKind.RoomMemberCount, is: "2" },
        { kind: ConditionKind.EventMatch, key: "type", pattern: "m.room.encrypted" },
    ],
    actions: [
        PushRuleActionName.Notify,
        { set_tweak: TweakName.Sound, value: "default" },
        { set_tweak: TweakName.Highlight, value: false },
    ],
    rule_id: ".m.rule.encrypted_room_one_to_one",
    default: true,
    enabled: true,
} as IPushRule;
const groupRule = {
    conditions: [{ kind: ConditionKind.EventMatch, key: "type", pattern: "m.room.message" }],
    actions: [
        PushRuleActionName.Notify,
        { set_tweak: TweakName.Sound, value: "default" },
        { set_tweak: TweakName.Highlight, value: false },
    ],
    rule_id: ".m.rule.message",
    default: true,
    enabled: true,
};
const encryptedGroupRule: IPushRule = {
    conditions: [{ kind: ConditionKind.EventMatch, key: "type", pattern: "m.room.encrypted" }],
    actions: [PushRuleActionName.DontNotify],
    rule_id: ".m.rule.encrypted",
    default: true,
    enabled: true,
} as IPushRule;

const bananaRule = {
    actions: [PushRuleActionName.Notify, { set_tweak: TweakName.Highlight, value: false }],
    pattern: "banana",
    rule_id: "banana",
    default: false,
    enabled: true,
} as IPushRule;

const pushRules: IPushRules = {
    global: {
        underride: [
            {
                conditions: [{ kind: ConditionKind.EventMatch, key: "type", pattern: "m.call.invite" }],
                actions: [
                    PushRuleActionName.Notify,
                    { set_tweak: TweakName.Sound, value: "ring" },
                    { set_tweak: TweakName.Highlight, value: false },
                ],
                rule_id: ".m.rule.call",
                default: true,
                enabled: true,
            },
            oneToOneRule,
            encryptedOneToOneRule,
            groupRule,
            encryptedGroupRule,
            {
                conditions: [
                    { kind: ConditionKind.EventMatch, key: "type", pattern: "im.vector.modular.widgets" },
                    { kind: ConditionKind.EventMatch, key: "content.type", pattern: "jitsi" },
                    { kind: ConditionKind.EventMatch, key: "state_key", pattern: "*" },
                ],
                actions: [PushRuleActionName.Notify, { set_tweak: TweakName.Highlight, value: false }],
                rule_id: ".im.vector.jitsi",
                default: true,
                enabled: true,
            },
        ],
        sender: [],
        room: [
            {
                actions: [PushRuleActionName.DontNotify],
                rule_id: "!zJPyWqpMorfCcWObge:matrix.org",
                default: false,
                enabled: true,
            },
        ],
        content: [
            bananaRule,
            {
                actions: [
                    PushRuleActionName.Notify,
                    { set_tweak: TweakName.Sound, value: "default" },
                    { set_tweak: TweakName.Highlight },
                ],
                pattern: "kadev1",
                rule_id: ".m.rule.contains_user_name",
                default: true,
                enabled: true,
            },
        ],
        override: [
            {
                conditions: [],
                actions: [PushRuleActionName.DontNotify],
                rule_id: ".m.rule.master",
                default: true,
                enabled: false,
            },
            {
                conditions: [{ kind: ConditionKind.EventMatch, key: "content.msgtype", pattern: "m.notice" }],
                actions: [PushRuleActionName.DontNotify],
                rule_id: ".m.rule.suppress_notices",
                default: true,
                enabled: true,
            },
            {
                conditions: [
                    { kind: ConditionKind.EventMatch, key: "type", pattern: "m.room.member" },
                    { kind: ConditionKind.EventMatch, key: "content.membership", pattern: "invite" },
                    { kind: ConditionKind.EventMatch, key: "state_key", pattern: "@kadev1:matrix.org" },
                ],
                actions: [
                    PushRuleActionName.Notify,
                    { set_tweak: TweakName.Sound, value: "default" },
                    { set_tweak: TweakName.Highlight, value: false },
                ],
                rule_id: ".m.rule.invite_for_me",
                default: true,
                enabled: true,
            },
            {
                conditions: [{ kind: ConditionKind.EventMatch, key: "type", pattern: "m.room.member" }],
                actions: [PushRuleActionName.DontNotify],
                rule_id: ".m.rule.member_event",
                default: true,
                enabled: true,
            },
            {
                conditions: [{ kind: "contains_display_name" }],
                actions: [
                    PushRuleActionName.Notify,
                    { set_tweak: TweakName.Sound, value: "default" },
                    { set_tweak: TweakName.Highlight },
                ],
                rule_id: ".m.rule.contains_display_name",
                default: true,
                enabled: true,
            },
            {
                conditions: [
                    { kind: ConditionKind.EventMatch, key: "content.body", pattern: "@room" },
                    { kind: "sender_notification_permission", key: "room" },
                ],
                actions: [PushRuleActionName.Notify, { set_tweak: TweakName.Highlight, value: true }],
                rule_id: ".m.rule.roomnotif",
                default: true,
                enabled: true,
            },
            {
                conditions: [
                    { kind: ConditionKind.EventMatch, key: "type", pattern: "m.room.tombstone" },
                    { kind: ConditionKind.EventMatch, key: "state_key", pattern: "" },
                ],
                actions: [PushRuleActionName.Notify, { set_tweak: TweakName.Highlight, value: true }],
                rule_id: ".m.rule.tombstone",
                default: true,
                enabled: true,
            },
            {
                conditions: [{ kind: ConditionKind.EventMatch, key: "type", pattern: "m.reaction" }],
                actions: [PushRuleActionName.DontNotify],
                rule_id: ".m.rule.reaction",
                default: true,
                enabled: true,
            },
        ],
    },
    device: {},
} as IPushRules;

const flushPromises = async () => await new Promise((resolve) => window.setTimeout(resolve));

describe("<Notifications />", () => {
    const getComponent = () =>
        render(
            <Form.Root>
                <Notifications />
            </Form.Root>,
        );

    // get component, wait for async data and force a render
    const getComponentAndWait = async () => {
        const component = getComponent();
        await waitForElementToBeRemoved(() => component.queryAllByRole("progressbar"));
        return component;
    };

    const mockClient = getMockClientWithEventEmitter({
        ...mockClientMethodsUser(),
        getPushRules: jest.fn(),
        getPushers: jest.fn(),
        getThreePids: jest.fn(),
        setPusher: jest.fn(),
        removePusher: jest.fn(),
        setPushRuleEnabled: jest.fn(),
        setPushRuleActions: jest.fn(),
        getRooms: jest.fn().mockReturnValue([]),
        getAccountData: jest.fn().mockImplementation((eventType) => {
            if (eventType.startsWith(LOCAL_NOTIFICATION_SETTINGS_PREFIX.name)) {
                return new MatrixEvent({
                    type: eventType,
                    content: {
                        is_silenced: false,
                    },
                });
            }
        }),
        setAccountData: jest.fn(),
        sendReadReceipt: jest.fn(),
        supportsThreads: jest.fn().mockReturnValue(true),
        isInitialSyncComplete: jest.fn().mockReturnValue(false),
        addPushRule: jest.fn().mockResolvedValue({}),
        deletePushRule: jest.fn().mockResolvedValue({}),
    });
    mockClient.getPushRules.mockResolvedValue(pushRules);

    beforeEach(async () => {
        let i = 0;
        mocked(secureRandomString).mockImplementation(() => {
            return "testid_" + i++;
        });

        mockClient.getPushRules.mockClear().mockResolvedValue(pushRules);
        mockClient.getPushers.mockClear().mockResolvedValue({ pushers: [] });
        mockClient.getThreePids.mockClear().mockResolvedValue({ threepids: [] });
        mockClient.setPusher.mockReset().mockResolvedValue({});
        mockClient.removePusher.mockClear().mockResolvedValue({});
        mockClient.setPushRuleActions.mockReset().mockResolvedValue({});
        mockClient.pushRules = pushRules;
        mockClient.getPushRules.mockClear().mockResolvedValue(pushRules);
        mockClient.addPushRule.mockClear();
        mockClient.deletePushRule.mockClear();
        // @ts-expect-error
        mockClient.pushProcessor = new PushProcessor(mockClient);

        userEvent.setup();

        await clearAllModals();
    });

    it("renders spinner while loading", async () => {
        getComponent();
        expect(screen.getByTestId("spinner")).toBeInTheDocument();
    });

    it("renders error message when fetching push rules fails", async () => {
        mockClient.getPushRules.mockRejectedValue({});
        await getComponentAndWait();
        expect(screen.getByTestId("error-message")).toBeInTheDocument();
    });
    it("renders error message when fetching pushers fails", async () => {
        mockClient.getPushers.mockRejectedValue({});
        await getComponentAndWait();
        expect(screen.getByTestId("error-message")).toBeInTheDocument();
    });
    it("renders error message when fetching threepids fails", async () => {
        mockClient.getThreePids.mockRejectedValue({});
        await getComponentAndWait();
        expect(screen.getByTestId("error-message")).toBeInTheDocument();
    });

    describe("main notification switches", () => {
        it("renders only enable notifications switch when notifications are disabled", async () => {
            const disableNotificationsPushRules = {
                global: {
                    ...pushRules.global,
                    override: [{ ...masterRule, enabled: true }],
                },
            } as unknown as IPushRules;
            mockClient.getPushRules.mockClear().mockResolvedValue(disableNotificationsPushRules);
            const { container } = await getComponentAndWait();

            expect(container).toMatchSnapshot();
        });
        it("renders switches correctly", async () => {
            await getComponentAndWait();

            expect(screen.getByLabelText("Enable notifications for this account")).toBeInTheDocument();
            expect(screen.getByLabelText("Enable notifications for this device")).toBeInTheDocument();
            expect(screen.getByLabelText("Enable desktop notifications for this session")).toBeInTheDocument();
            expect(screen.getByLabelText("Show message in desktop notification")).toBeInTheDocument();
            expect(screen.getByLabelText("Enable audible notifications for this session")).toBeInTheDocument();
        });

        describe("email switches", () => {
            const testEmail = "tester@test.com";
            beforeEach(() => {
                mockClient.getThreePids.mockResolvedValue({
                    threepids: [
                        // should render switch bc pushKey and address match
                        {
                            medium: ThreepidMedium.Email,
                            address: testEmail,
                        } as unknown as IThreepid,
                    ],
                });
            });

            it("renders email switches correctly when email 3pids exist", async () => {
                await getComponentAndWait();
                expect(screen.getByLabelText(`Enable email notifications for ${testEmail}`)).toBeInTheDocument();
            });

            it("renders email switches correctly when notifications are on for email", async () => {
                mockClient.getPushers.mockResolvedValue({
                    pushers: [{ kind: "email", pushkey: testEmail } as unknown as IPusher],
                });
                await getComponentAndWait();

                const emailSwitch = screen.getByLabelText(`Enable email notifications for ${testEmail}`);
                expect(emailSwitch).toBeChecked();
            });

            it("enables email notification when toggling on", async () => {
                await getComponentAndWait();

                const emailToggle = screen.getByLabelText(`Enable email notifications for ${testEmail}`);
                fireEvent.click(emailToggle);

                expect(mockClient.setPusher).toHaveBeenCalledWith(
                    expect.objectContaining({
                        kind: "email",
                        app_id: "m.email",
                        pushkey: testEmail,
                        app_display_name: "Email Notifications",
                        device_display_name: testEmail,
                        append: true,
                    }),
                );
            });

            it("displays error when pusher update fails", async () => {
                mockClient.setPusher.mockRejectedValue({});
                await getComponentAndWait();

                const emailToggle = screen.getByLabelText(`Enable email notifications for ${testEmail}`);
                fireEvent.click(emailToggle);

                // force render
                await flushPromises();

                const dialog = await screen.findByRole("dialog");

                expect(
                    within(dialog).getByText("An error occurred whilst saving your notification preferences."),
                ).toBeInTheDocument();

                // dismiss the dialog
                fireEvent.click(within(dialog).getByText("OK"));
                expect(screen.getByTestId("error-message")).toBeInTheDocument();
            });

            it("enables email notification when toggling off", async () => {
                const testPusher = {
                    kind: "email",
                    pushkey: "tester@test.com",
                    app_id: "testtest",
                } as unknown as IPusher;
                mockClient.getPushers.mockResolvedValue({ pushers: [testPusher] });
                await getComponentAndWait();

                const emailToggle = screen.getByLabelText(`Enable email notifications for ${testEmail}`);
                fireEvent.click(emailToggle);

                expect(mockClient.removePusher).toHaveBeenCalledWith(testPusher.pushkey, testPusher.app_id);
            });
        });

        it("toggles master switch correctly", async () => {
            await getComponentAndWait();

            // master switch is on
            expect(screen.getByLabelText("Enable notifications for this account")).toBeChecked();
            fireEvent.click(screen.getByLabelText("Enable notifications for this account"));

            await flushPromises();

            expect(mockClient.setPushRuleEnabled).toHaveBeenCalledWith("global", "override", ".m.rule.master", true);
        });

        it("toggles and sets settings correctly", async () => {
            await getComponentAndWait();
            let audioNotifsToggle!: HTMLInputElement;

            const update = () => {
                audioNotifsToggle = screen.getByLabelText("Enable audible notifications for this session");
            };
            update();

            expect(audioNotifsToggle).toBeChecked();
            expect(SettingsStore.getValue("audioNotificationsEnabled")).toEqual(true);

            fireEvent.click(audioNotifsToggle);
            update();

            expect(audioNotifsToggle).not.toBeChecked();
            expect(SettingsStore.getValue("audioNotificationsEnabled")).toEqual(false);
        });
    });

    describe("individual notification level settings", () => {
        it("renders categories correctly", async () => {
            await getComponentAndWait();

            expect(screen.getByTestId("notif-section-vector_global")).toBeInTheDocument();
            expect(screen.getByTestId("notif-section-vector_mentions")).toBeInTheDocument();
            expect(screen.getByTestId("notif-section-vector_other")).toBeInTheDocument();
        });

        it("renders radios correctly", async () => {
            await getComponentAndWait();
            const section = "vector_global";

            const globalSection = screen.getByTestId(`notif-section-${section}`);
            // 4 notification rules with class 'global'
            expect(globalSection.querySelectorAll("fieldset").length).toEqual(4);
            // oneToOneRule is set to 'on'
            const oneToOneRuleElement = screen.getByTestId(section + oneToOneRule.rule_id);
            expect(oneToOneRuleElement.querySelector("[aria-label='On']")).toBeInTheDocument();
            // encryptedOneToOneRule is set to 'loud'
            const encryptedOneToOneElement = screen.getByTestId(section + encryptedOneToOneRule.rule_id);
            expect(encryptedOneToOneElement.querySelector("[aria-label='Noisy']")).toBeInTheDocument();
            // encryptedGroupRule is set to 'off'
            const encryptedGroupElement = screen.getByTestId(section + encryptedGroupRule.rule_id);
            expect(encryptedGroupElement.querySelector("[aria-label='Off']")).toBeInTheDocument();
        });

        it("updates notification level when changed", async () => {
            await getComponentAndWait();
            const section = "vector_global";

            // oneToOneRule is set to 'on'
            // and is kind: 'underride'
            const oneToOneRuleElement = screen.getByTestId(section + oneToOneRule.rule_id);

            await act(async () => {
                const offToggle = oneToOneRuleElement.querySelector('input[type="radio"]')!;
                fireEvent.click(offToggle);
            });

            expect(mockClient.setPushRuleEnabled).toHaveBeenCalledWith(
                "global",
                "underride",
                oneToOneRule.rule_id,
                true,
            );

            // actions for '.m.rule.room_one_to_one' state is ACTION_DONT_NOTIFY
            expect(mockClient.setPushRuleActions).toHaveBeenCalledWith(
                "global",
                "underride",
                oneToOneRule.rule_id,
                StandardActions.ACTION_DONT_NOTIFY,
            );
        });

        it("adds an error message when updating notification level fails", async () => {
            await getComponentAndWait();
            const section = "vector_global";

            const error = new Error("oups");
            mockClient.setPushRuleEnabled.mockRejectedValue(error);

            // oneToOneRule is set to 'on'
            // and is kind: 'underride'
            const offToggle = screen.getByTestId(section + oneToOneRule.rule_id).querySelector('input[type="radio"]')!;
            await act(() => {
                fireEvent.click(offToggle);
            });

            await flushPromises();

            // error message attached to oneToOne rule
            const oneToOneRuleElement = screen.getByTestId(section + oneToOneRule.rule_id);
            // old value still shown as selected
            expect(within(oneToOneRuleElement).getByLabelText("On")).toBeChecked();
            expect(
                within(oneToOneRuleElement).getByText(
                    "An error occurred when updating your notification preferences. Please try to toggle your option again.",
                ),
            ).toBeInTheDocument();
        });

        it("clears error message for notification rule on retry", async () => {
            await getComponentAndWait();
            const section = "vector_global";

            const error = new Error("oups");
            mockClient.setPushRuleEnabled.mockRejectedValueOnce(error).mockResolvedValue({});

            // oneToOneRule is set to 'on'
            // and is kind: 'underride'
            const offToggle = screen.getByTestId(section + oneToOneRule.rule_id).querySelector('input[type="radio"]')!;
            await act(() => {
                fireEvent.click(offToggle);
            });

            await flushPromises();

            // error message attached to oneToOne rule
            const oneToOneRuleElement = screen.getByTestId(section + oneToOneRule.rule_id);
            expect(
                within(oneToOneRuleElement).getByText(
                    "An error occurred when updating your notification preferences. Please try to toggle your option again.",
                ),
            ).toBeInTheDocument();

            // retry
            fireEvent.click(offToggle);

            // error removed as soon as we start request
            expect(
                within(oneToOneRuleElement).queryByText(
                    "An error occurred when updating your notification preferences. Please try to toggle your option again.",
                ),
            ).not.toBeInTheDocument();

            await flushPromises();

            // no error after successful change
            expect(
                within(oneToOneRuleElement).queryByText(
                    "An error occurred when updating your notification preferences. Please try to toggle your option again.",
                ),
            ).not.toBeInTheDocument();
        });

        describe("synced rules", () => {
            const pollStartOneToOne = {
                conditions: [
                    {
                        kind: ConditionKind.RoomMemberCount,
                        is: "2",
                    } as IPushRuleCondition<ConditionKind.RoomMemberCount>,
                    {
                        kind: ConditionKind.EventMatch,
                        key: "type",
                        pattern: "org.matrix.msc3381.poll.start",
                    } as IPushRuleCondition<ConditionKind.EventMatch>,
                ],
                actions: [PushRuleActionName.DontNotify],
                rule_id: ".org.matrix.msc3930.rule.poll_start_one_to_one",
                default: true,
                enabled: true,
            } as IPushRule;
            const pollStartGroup = {
                conditions: [
                    {
                        kind: ConditionKind.EventMatch,
                        key: "type",
                        pattern: "org.matrix.msc3381.poll.start",
                    },
                ],
                actions: [PushRuleActionName.Notify],
                rule_id: ".org.matrix.msc3930.rule.poll_start",
                default: true,
                enabled: true,
            } as IPushRule;
            const pollEndOneToOne = {
                conditions: [
                    {
                        kind: ConditionKind.RoomMemberCount,
                        is: "2",
                    },
                    {
                        kind: ConditionKind.EventMatch,
                        key: "type",
                        pattern: "org.matrix.msc3381.poll.end",
                    },
                ],
                actions: [
                    PushRuleActionName.Notify,
                    { set_tweak: TweakName.Highlight, value: false },
                    { set_tweak: TweakName.Sound, value: "default" },
                ],
                rule_id: ".org.matrix.msc3930.rule.poll_end_one_to_one",
                default: true,
                enabled: true,
            } as IPushRule;

            const setPushRuleMock = (rules: IPushRule[] = []): void => {
                const combinedRules = {
                    ...pushRules,
                    global: {
                        ...pushRules.global,
                        underride: [...pushRules.global.underride!, ...rules],
                    },
                };
                mockClient.getPushRules.mockClear().mockResolvedValue(combinedRules);
                mockClient.pushRules = combinedRules;
            };

            // ".m.rule.room_one_to_one" and ".m.rule.message" have synced rules
            it("succeeds when no synced rules exist for user", async () => {
                await getComponentAndWait();
                const section = "vector_global";

                const oneToOneRuleElement = screen.getByTestId(section + oneToOneRule.rule_id);

                const offToggle = oneToOneRuleElement.querySelector('input[type="radio"]')!;
                fireEvent.click(offToggle);

                await flushPromises();

                // didnt attempt to update any non-existant rules
                expect(mockClient.setPushRuleActions).toHaveBeenCalledTimes(1);

                // no error
                expect(
                    within(oneToOneRuleElement).queryByText(
                        "An error occurred when updating your notification preferences. Please try to toggle your option again.",
                    ),
                ).not.toBeInTheDocument();
            });

            it("updates synced rules when they exist for user", async () => {
                setPushRuleMock([pollStartOneToOne, pollStartGroup]);
                await getComponentAndWait();
                const section = "vector_global";
                const oneToOneRuleElement = screen.getByTestId(section + oneToOneRule.rule_id);

                const offToggle = oneToOneRuleElement.querySelector('input[type="radio"]')!;
                fireEvent.click(offToggle);

                await flushPromises();

                // updated synced rule
                expect(mockClient.setPushRuleActions).toHaveBeenCalledWith(
                    "global",
                    "underride",
                    oneToOneRule.rule_id,
                    [PushRuleActionName.DontNotify],
                );
                expect(mockClient.setPushRuleActions).toHaveBeenCalledWith(
                    "global",
                    "underride",
                    pollStartOneToOne.rule_id,
                    [PushRuleActionName.DontNotify],
                );
                // only called for parent rule and one existing synced rule
                expect(mockClient.setPushRuleActions).toHaveBeenCalledTimes(2);

                // no error
                expect(
                    within(oneToOneRuleElement).queryByText(
                        "An error occurred when updating your notification preferences. Please try to toggle your option again.",
                    ),
                ).not.toBeInTheDocument();
            });

            it("does not update synced rules when main rule update fails", async () => {
                setPushRuleMock([pollStartOneToOne]);
                await getComponentAndWait();
                const section = "vector_global";
                const oneToOneRuleElement = screen.getByTestId(section + oneToOneRule.rule_id);
                // have main rule update fail
                mockClient.setPushRuleActions.mockRejectedValue("oups");

                const offToggle = oneToOneRuleElement.querySelector('input[type="radio"]')!;
                await act(() => {
                    fireEvent.click(offToggle);
                });

                await flushPromises();

                expect(mockClient.setPushRuleActions).toHaveBeenCalledWith(
                    "global",
                    "underride",
                    oneToOneRule.rule_id,
                    [PushRuleActionName.DontNotify],
                );
                // only called for parent rule
                expect(mockClient.setPushRuleActions).toHaveBeenCalledTimes(1);

                expect(
                    within(oneToOneRuleElement).getByText(
                        "An error occurred when updating your notification preferences. Please try to toggle your option again.",
                    ),
                ).toBeInTheDocument();
            });

            it("sets the UI toggle to rule value when no synced rule exist for the user", async () => {
                setPushRuleMock([]);
                await getComponentAndWait();
                const section = "vector_global";
                const oneToOneRuleElement = screen.getByTestId(section + oneToOneRule.rule_id);

                // loudest state of synced rules should be the toggle value
                expect(oneToOneRuleElement.querySelector('input[aria-label="On"]')).toBeChecked();
            });

            it("sets the UI toggle to the loudest synced rule value", async () => {
                // oneToOneRule is set to 'On'
                // pollEndOneToOne is set to 'Loud'
                setPushRuleMock([pollStartOneToOne, pollEndOneToOne]);
                await getComponentAndWait();
                const section = "vector_global";
                const oneToOneRuleElement = screen.getByTestId(section + oneToOneRule.rule_id);

                // loudest state of synced rules should be the toggle value
                expect(oneToOneRuleElement.querySelector('input[aria-label="Noisy"]')).toBeChecked();

                const onToggle = oneToOneRuleElement.querySelector('input[aria-label="On"]')!;
                fireEvent.click(onToggle);

                await flushPromises();

                // called for all 3 rules
                expect(mockClient.setPushRuleActions).toHaveBeenCalledTimes(3);
                const expectedActions = [PushRuleActionName.Notify, { set_tweak: TweakName.Highlight, value: false }];
                expect(mockClient.setPushRuleActions).toHaveBeenCalledWith(
                    "global",
                    "underride",
                    oneToOneRule.rule_id,
                    expectedActions,
                );
                expect(mockClient.setPushRuleActions).toHaveBeenCalledWith(
                    "global",
                    "underride",
                    pollStartOneToOne.rule_id,
                    expectedActions,
                );
                expect(mockClient.setPushRuleActions).toHaveBeenCalledWith(
                    "global",
                    "underride",
                    pollEndOneToOne.rule_id,
                    expectedActions,
                );
            });
        });
    });

    describe("keywords", () => {
        // keywords rule is not a real rule, but controls actions on keywords content rules
        const keywordsRuleId = "_keywords";
        it("updates individual keywords content rules when keywords rule is toggled", async () => {
            await getComponentAndWait();
            const section = "vector_mentions";

            fireEvent.click(within(screen.getByTestId(section + keywordsRuleId)).getByLabelText("Off"));

            expect(mockClient.setPushRuleEnabled).toHaveBeenCalledWith("global", "content", bananaRule.rule_id, false);

            fireEvent.click(within(screen.getByTestId(section + keywordsRuleId)).getByLabelText("Noisy"));

            expect(mockClient.setPushRuleActions).toHaveBeenCalledWith(
                "global",
                "content",
                bananaRule.rule_id,
                StandardActions.ACTION_HIGHLIGHT_DEFAULT_SOUND,
            );
        });

        it("renders an error when updating keywords fails", async () => {
            await getComponentAndWait();
            const section = "vector_mentions";

            mockClient.setPushRuleEnabled.mockRejectedValueOnce("oups");

            await act(() => {
                fireEvent.click(within(screen.getByTestId(section + keywordsRuleId)).getByLabelText("Off"));
            });

            await flushPromises();

            const rule = screen.getByTestId(section + keywordsRuleId);

            expect(
                within(rule).getByText(
                    "An error occurred when updating your notification preferences. Please try to toggle your option again.",
                ),
            ).toBeInTheDocument();
        });

        it("adds a new keyword", async () => {
            await getComponentAndWait();

            await userEvent.type(screen.getByLabelText("Keyword"), "jest");
            expect(screen.getByLabelText("Keyword")).toHaveValue("jest");

            fireEvent.click(screen.getByText("Add"));

            expect(mockClient.addPushRule).toHaveBeenCalledWith("global", PushRuleKind.ContentSpecific, "jest", {
                actions: [PushRuleActionName.Notify, { set_tweak: "highlight", value: false }],
                pattern: "jest",
            });
        });

        it("adds a new keyword with same actions as existing rules when keywords rule is off", async () => {
            const offContentRule = {
                ...bananaRule,
                enabled: false,
                actions: [PushRuleActionName.Notify],
            };
            const pushRulesWithContentOff = {
                global: {
                    ...pushRules.global,
                    content: [offContentRule],
                },
            };
            mockClient.pushRules = pushRulesWithContentOff;
            mockClient.getPushRules.mockClear().mockResolvedValue(pushRulesWithContentOff);

            await getComponentAndWait();

            const keywords = screen.getByTestId("vector_mentions_keywords");

            expect(within(keywords).getByLabelText("Off")).toBeChecked();

            await userEvent.type(screen.getByLabelText("Keyword"), "jest");
            expect(screen.getByLabelText("Keyword")).toHaveValue("jest");

            fireEvent.click(screen.getByText("Add"));

            expect(mockClient.addPushRule).toHaveBeenCalledWith("global", PushRuleKind.ContentSpecific, "jest", {
                actions: [PushRuleActionName.Notify, { set_tweak: TweakName.Highlight, value: false }],
                pattern: "jest",
            });
        });

        it("removes keyword", async () => {
            await getComponentAndWait();

            await userEvent.type(screen.getByLabelText("Keyword"), "jest");

            const keyword = screen.getByText("banana");

            fireEvent.click(within(keyword.parentElement!).getByLabelText("Remove"));

            expect(mockClient.deletePushRule).toHaveBeenCalledWith("global", PushRuleKind.ContentSpecific, "banana");

            await flushPromises();
        });
    });

    describe("clear all notifications", () => {
        it("clears all notifications", async () => {
            const room = new Room("room123", mockClient, "@alice:example.org");
            mockClient.getRooms.mockReset().mockReturnValue([room]);

            const message = mkMessage({
                event: true,
                room: "room123",
                user: "@alice:example.org",
                ts: 1,
            });
            await room.addLiveEvents([message], { addToState: true });

            const { container } = await getComponentAndWait();
            const clearNotificationEl = getByTestId(container, "clear-notifications");

            fireEvent.click(clearNotificationEl);

            expect(clearNotificationEl.className).toContain("mx_AccessibleButton_disabled");
            await waitFor(() => expect(clearNotificationEl.className).not.toContain("mx_AccessibleButton_disabled"));
            expect(mockClient.sendReadReceipt).toHaveBeenCalled();
        });
    });
});
